﻿namespace StyleCop.CSharp
{
    /// <summary>
    /// Holds information about a variables usage.
    /// </summary>
    public class VariableUsageInformation
    {
        private readonly ICodeUnit hoistedInside;
        private readonly LiteralExpression source;
        private readonly VariableUsage usage;
        private readonly Variable variable;

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="VariableUsageInformation"/> class.
        /// </summary>
        /// <param name="source">The source of the variable usage.</param>
        /// <param name="variable">The variable.</param>
        /// <param name="usage">The variables usage.</param>
        /// <param name="hoistedInside">The code unit the varialble hoisted inside.</param>
        public VariableUsageInformation(LiteralExpression source, Variable variable, VariableUsage usage, ICodeUnit hoistedInside)
        {
            this.source = source;
            this.variable = variable;
            this.usage = usage;
            this.hoistedInside = hoistedInside;
        }
        #endregion Constructors

        #region Public Properties
        /// <summary>
        /// Gets a value indicating whether this instance is hoisted.
        /// </summary>
        /// <value><c>true</c> if this instance is hoisted.</value>
        /// <remarks>
        /// A hoisted variable is one that is used in a lambda expression or an anonymous method.
        /// </remarks>
        public ICodeUnit HoistedInside
        {
            get { return this.hoistedInside; }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is catch variable.
        /// </summary>
        /// <value><c>true</c> if this instance is catch variable.</value>
        public bool IsCatchVariable
        {
            get
            {
                return this.IsDeclaration
                    && (this.Source.ParentStatement() is CatchStatement);
            }
        }

        /// <summary>
        /// Gets a value indicating whether this variable usage is a declaration.
        /// </summary>
        /// <value><c>true</c> if this variable usage is a declaration.</value>
        public bool IsDeclaration
        {
            get
            {
                return this.Usage == VariableUsage.Declaration
                    || this.Usage == VariableUsage.DeclarationAndAssignment;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is for variable.
        /// </summary>
        /// <value><c>true</c> if this instance is for variable.</value>
        public bool IsForVariable
        {
            get
            {
                return this.IsDeclaration
                    && this.Source.ParentStatement() is ForeachStatement;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is the variable in a using statement.
        /// </summary>
        /// <value><c>true</c> if this instance is the variable in a using statement.</value>
        public bool IsUsingVariable
        {
            get
            {
                return this.IsDeclaration
                    && this.Source.ParentStatement() is UsingStatement;
            }
        }

        /// <summary>
        /// Gets the source of the variable usage.
        /// </summary>
        /// <value>The source of the variable usage.</value>
        public LiteralExpression Source
        {
            get { return this.source; }
        }

        /// <summary>
        /// Gets the usage of the variable.
        /// </summary>
        /// <value>The usage of the variable.</value>
        public VariableUsage Usage
        {
            get { return this.usage; }
        }

        /// <summary>
        /// Gets the variable.
        /// </summary>
        /// <value>The variable.</value>
        public Variable Variable
        {
            get { return this.variable; }
        }
        #endregion Public Properties

        #region Public Methods
        /// <summary>
        /// Get the smallest variable scope for this variable usage.
        /// </summary>
        /// <returns>The smallest variable scope for this variable usage.</returns>
        public ICodeUnit SmallestVariableScope()
        {
            ICodeUnit codeUnit = this.InnerSmallestVariableScope();
            if (codeUnit == null)
            {
                return this.Source.ContainingElement();
            }
            else
            {
                return EscalateBlockToContainingStatement(codeUnit);
            }
        }

        /// <summary>
        /// Get the smallest variable scope for this variable usage.
        /// </summary>
        /// <param name="currentScope">The current variable scope.</param>
        /// <returns>
        /// The smallest variable scope for this variable usage and the specified currentScope.
        /// </returns>
        public ICodeUnit SmallestVariableScope(ICodeUnit currentScope)
        {
            ICodeUnit smallestScope = this.SmallestVariableScope();
            if (smallestScope == null)
            {
                return this.Source.ContainingElement();
            }
            else
            {
                ICodeUnit closestAncestor = smallestScope.ClosestCommonAncestor(currentScope);
                if (closestAncestor == null)
                {
                    return this.Source.ContainingElement();
                }
                else
                {
                    return EscalateBlockToContainingStatement(closestAncestor);
                }
            }
        }
        #endregion Public Methods

        #region Private Methods
        private static ICodeUnit EscalateBlockToContainingStatement(ICodeUnit codeUnit)
        {
            BlockStatement blockStatement = codeUnit as BlockStatement;
            if (blockStatement != null)
            {
                Statement parentStatement = blockStatement.ParentStatement();
                if (parentStatement != null)
                {
                    switch (parentStatement.StatementType)
                    {
                        case StatementType.Catch:
                        case StatementType.DoWhile:
                        case StatementType.Else:
                        case StatementType.Finally:
                        case StatementType.For:
                        case StatementType.Foreach:
                        case StatementType.If:
                        case StatementType.Switch:
                        case StatementType.Try:
                        case StatementType.Using:
                        case StatementType.While:
                            return parentStatement;
                    }
                }
            }

            return codeUnit;
        }

        /// <summary>
        /// Get the smallest variable scope for this variable usage.
        /// </summary>
        /// <returns>The smallest variable scope for this variable usage.</returns>
        private ICodeUnit InnerSmallestVariableScope()
        {
            Statement parentStatement = this.Source.ParentStatement();
            Statement grandparentStatement = parentStatement.ParentStatement();

            if (this.IsDeclaration)
            {
                if (parentStatement.StatementType == StatementType.VariableDeclaration)
                {
                    // If this is the variable in a declaration statement then return the statement's container.
                    return grandparentStatement;
                }
                else
                {
                    // If the variable is declared inside another statement then return that statement. This is
                    // probably a for, foreach, using, etc.
                    return parentStatement;
                }
            }

            // If the variable is declared in the for statement and used in the statement then it's scope is the for statement.
            if (parentStatement.StatementType == StatementType.For)
            {
                ForStatement forStatement = (ForStatement)parentStatement;
                if (forStatement.Variables.Contains(this.Variable.Name))
                {
                    return parentStatement;
                }
            }

            return grandparentStatement;
        }
        #endregion Private Methods
    }
}